﻿using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Notoric.Commanding.CommandBase;
using System.Windows;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.Barline"/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public class Barline : ViewModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Barline"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.Barline"/>
        /// </para>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        public Barline(System owner, Model.Elements.CWNotation.Barline adaptedElement)
            : base(adaptedElement, owner, owner.UndoStack, owner.RedoStack)
        {
            segments = new ObservableCollection<BarlineSegment>();

            var modelSectionInterior = AdaptedElement.FindOwner<Model.Elements.MusicSectionElement>();
            
            foreach (var partReference in modelSectionInterior.Parts)
            {
                var cwData = partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>();

                foreach (var segment in cwData.BarlineSegments)
                    segments.Add(new BarlineSegment(this, segment));

                cwData.BarlineSegments.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Segments_CollectionChanged);
            }

            modelSectionInterior.Parts.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(PresentedParts_CollectionChanged);
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override void Dispose()
        {
            var modelSectionInterior = AdaptedElement.FindOwner<Model.Elements.MusicSectionElement>();

            foreach (var partReference in modelSectionInterior.Parts)
                partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>().BarlineSegments.CollectionChanged -= Segments_CollectionChanged;

            modelSectionInterior.Parts.CollectionChanged -= PresentedParts_CollectionChanged;

            foreach (var segment in segments)
                segment.Dispose();

            base.Dispose();
        }

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the collection of the presented parts has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void PresentedParts_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                    foreach (Model.Elements.PartDescriptorElement partReference in e.NewItems)
                    {
                        var cwData = partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>();

                        foreach (var segment in cwData.BarlineSegments)
                            segments.Add(new BarlineSegment(this, segment));

                        cwData.BarlineSegments.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Segments_CollectionChanged);
                    }
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.PartDescriptorElement partReference in e.OldItems)
                    {
                        var cwData = partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>();

                        foreach (var segment in cwData.BarlineSegments)
                        {
                            var removed = segments.First(s => ((Model.Elements.CWNotation.BarlineSegment)s.AdaptedElement) == segment);
                            segments.Remove(removed);
                            removed.Dispose();
                        }

                        cwData.BarlineSegments.CollectionChanged -= Segments_CollectionChanged;
                    }
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                    foreach (Model.Elements.PartDescriptorElement partReference in e.OldItems)
                    {
                        var cwData = partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>();

                        foreach (var segment in cwData.BarlineSegments)
                        {
                            var removed = segments.First(s => ((Model.Elements.CWNotation.BarlineSegment)s.AdaptedElement) == segment);
                            segments.Remove(removed);
                            removed.Dispose();
                        }

                        cwData.BarlineSegments.CollectionChanged -= Segments_CollectionChanged;
                    }
                    foreach (Model.Elements.PartDescriptorElement partReference in e.NewItems)
                    {
                        var cwData = partReference.FindCustomData<Model.Elements.CWNotation.CWPartData>();

                        foreach (var segment in cwData.BarlineSegments)
                            segments.Add(new BarlineSegment(this, segment));

                        cwData.BarlineSegments.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Segments_CollectionChanged);
                    }
                    break;
            }
        }

        /// <summary>
        /// Occurs when the collection of barline segments for some part descriptor has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Segments_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                    foreach (Model.Elements.CWNotation.BarlineSegment segment in e.NewItems)
                        segments.Add(new BarlineSegment(this, segment));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.BarlineSegment segment in e.OldItems)
                    {
                        var removed = segments.First(s => s.AdaptedElement == segment);
                        segments.Remove(removed);
                        removed.Dispose();
                    }
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                    foreach (Model.Elements.CWNotation.BarlineSegment segment in e.OldItems)
                    {
                        var removed = segments.First(s => s.AdaptedElement == segment);
                        segments.Remove(removed);
                        removed.Dispose();
                    }
                    foreach (Model.Elements.CWNotation.BarlineSegment segment in e.NewItems)
                        segments.Add(new BarlineSegment(this, segment));
                    break;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Position"/> of the barline.
        /// </summary>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new position of the barline (in device independent pixels).
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetPositionCommand(double position)
        {
            var modelPosition = (double)LengthConverter.ConvertBack(position, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "Position", modelPosition, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Type"/> of the barline.
        /// </summary>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="BarlineType"/>
        /// </para>
        /// <para>
        /// The new type of the barline.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetTypeCommand(BarlineType type)
        {
            var modelType = (Model.Elements.CWNotation.BarlineType)Enum.Parse(typeof(Model.Elements.CWNotation.BarlineType), type.ToString());

            return new SetterCommand<Model.Elements.CWNotation.BarlineType>(this, "Type", modelType, UndoStack, RedoStack);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the position of the barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The distance (in device independent pixels) between the left edge of the system and the barline.
        /// </para>
        /// </value>
        public double Position
        {
            get { return FindOwner<System>().Left + (double)LengthConverter.Convert(GetPropertyValue<double>("Position"), typeof(double), null, CultureInfo.CurrentCulture); ; }
            set
            {
                if (UnsavedChanges.ContainsKey("Position"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Position"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Position"] = CreateSetPositionCommand(value);

                RaisePropertyChanged("Position");
            }
        }

        /// <summary>
        /// Gets the collection of the segments of this barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="BarlineSegment"/>
        /// </para>
        /// <para>
        /// The segments of this barline.
        /// </para>
        /// </value>
        public ObservableCollection<BarlineSegment> Segments
        {
            get { return segments; }
        }

        /// <summary>
        /// Gets or sets the type of the barline.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="BarlineType"/>
        /// </para>
        /// <para>
        /// The type of the barline.
        /// </para>
        /// </value>
        public BarlineType Type
        {
            get 
            { 
                var modelType = GetPropertyValue<Model.Elements.CWNotation.BarlineType>("Type");
                
                return (BarlineType)Enum.Parse(typeof(BarlineType), modelType.ToString());
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Type"))
                {
                    var cmd = (SetterCommand<Model.Elements.CWNotation.BarlineType>)UnsavedChanges["Type"];
                    cmd.Value = (Model.Elements.CWNotation.BarlineType)Enum.Parse(typeof(Model.Elements.CWNotation.BarlineType), value.ToString());
                }
                else
                    UnsavedChanges["Type"] = CreateSetTypeCommand(value);

                RaisePropertyChanged("Type");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.Barline";
        /// <summary>
        /// Holds the value of the <see cref="Segments"/> property.
        /// </summary>
        private ObservableCollection<BarlineSegment> segments;
        
        #endregion
    }

    /// <summary>
    /// Describes the type of the barline.
    /// </summary>
    public enum BarlineType
    {
        /// <summary>
        /// Single barline.
        /// </summary>
        Single,
        /// <summary>
        /// Left repetition barline.
        /// </summary>
        RepetitionOpen,
        /// <summary>
        /// Right repetition barline.
        /// </summary>
        RepetitionClose,
        /// <summary>
        /// Repetition open on left side and repetition close on right side.
        /// </summary>
        RepetitionBoth,
        /// <summary>
        /// Double barline.
        /// </summary>
        Double,
        /// <summary>
        /// Final barline.
        /// </summary>
        Final
    }
}
